package bluknite.util.network;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.net.Socket;
import java.util.logging.Logger;

import bluknite.util.network.exception.IncompleteConnectionException;
import bluknite.util.network.exception.NetworkException;
import bluknite.util.network.exception.SendFailedException;

/**
 * This class represents a TCP Connection and provides
 * send and recieve capabilities over the TCP Connection.
 * <br><br>
 * The class must be initialized with an <i>in</i> and
 * <i>out</i> sockets as well as a {@link TCPConnectionHandler}
 * instance.
 *  
 * @author Siraj Ahmed
 */
public class TCPConnection {
	/** I/O sockets **/
	private Socket inSocket;
	private Socket outSocket;
	/** Thread to watch for remote site input **/
	private ReadThread reader;
	/** TCPConnectionHandler instance **/
	private TCPConnectionHandler handler;
	/** I/O object streams **/
	private ObjectInputStream in;
	private ObjectOutputStream out;
	/** Used to stop ReadThread **/
	private boolean running = false;
	private boolean connected = false;
	
	private Logger logger = Logger.getLogger(this.getClass().getName());
	
	/**
	 * Instantiates a {@code TCPConnection} instance using
	 * the provided {@code in} and {@code out} sockets and
	 * {@code handler}.
	 * @param in the {@code Socket} used to recieve data
	 * from the remote site.
	 * @param out the {@code Socket} used to send data to
	 * the remote site
	 * @param handler the {@code TCPConnectionHandler} implementing
	 * instance that will handle processing of data recieved
	 * from the remote site as well as initial connection and
	 * final disconnection processes.<br>
	 * If {@code handler} is {@code null} then all recieved
	 * data is ignored.
	 * @throws NetworkException if either {@code in} or
	 * {@code out} are null.
	 */
	protected TCPConnection(Socket in, Socket out, TCPConnectionHandler handler) throws NetworkException {
		if((in == null)||(!in.isConnected())) {
			throw new IncompleteConnectionException("Inbound socket is null/not connected!");
		}
		if((out == null)||(!out.isConnected())) {
			throw new IncompleteConnectionException("Outbound socket is null/not connected!");
		}
		inSocket = in;
		outSocket = out;
		this.handler = handler;
		if(handler != null) {
			this.handler.setConnection(this);
		}
		handler.connectionProcess();
		running = true;
		reader = new ReadThread();
		reader.start();
		connected = true;
	}
	
	/**
	 * Sends a {@code Serializable} object to the remote site.
	 * @param o the object to send.
	 * @throws NetworkException if there was an error sending
	 * the object.
	 */
	public void send(Serializable o) throws NetworkException {
		try {
			out = new ObjectOutputStream(outSocket.getOutputStream());
			out.writeObject(o);
			out.flush();
		} catch(IOException e) {
			logger.warning(e.toString());
			throw new SendFailedException("Failed to send! " + e.getMessage());
		}
	}
	
	/**
	 * Disconnects this {@code TCPConnection} instance,
	 * closing all associated resources.
	 */
	public void disconnect() {
		running = false;
		if(connected) {
			handler.disconnectionProcess();
			closeResources();
		}
	}
	
	/**
	 * Closes all resources associated with this
	 * {@code TCPConnection} instance.
	 */
	private void closeResources() {
		try {
			out.close();
		} catch (IOException e) {
			logger.severe(e.toString());
		} catch(Exception e) {}
		try {
			in.close();
		} catch (IOException e) {
			logger.severe(e.toString());
		} catch(Exception e) {}
		try {
			inSocket.close();
		} catch (IOException e) {
			logger.severe(e.toString());
		}
		try {
			outSocket.close();
		} catch (IOException e) {
			logger.severe(e.toString());
		}
		connected = false;
	}
	
	public boolean isConnected() {
		return connected;
	}
	
	/**
	 * Private inner {@code Thread} class that is
	 * used to watch for and process data sent from
	 * the remote site.
	 * @author Siraj Ahmed
	 */
	private class ReadThread extends Thread {

		/**
		 * Repeatedly tries to read an object from the
		 * remote site and passes it to the {@code handler}
		 * for processing. 
		 */
		public void run() {
			while(running) {
				try {
					in = new ObjectInputStream(inSocket.getInputStream());
					Object o = in.readObject();
					if(handler != null) {
						handler.processMessage(o);
					}
				} catch (IOException e) {
					if(connected) {
						logger.info(e.toString());
						disconnect();
					}
				} catch (ClassNotFoundException e) {
					logger.severe(e.toString());
				}
			}
			super.run();
		}
	}
}
